#!/usr/bin/ruby

require 'erb'
require 'ostruct'

RELPATH = File.expand_path(File.dirname(__FILE__))
BUILDPATH = RELPATH + "/build"

puts RUBY_VERSION
['build'].each { |dirname| Dir.mkdir(dirname) unless File.exist?(dirname) }

class ProtobufMagician

  def initialize
    @packages = []
    @instance_variables = {}
    @nested_messages = {}
    @nested_messages_ivs = {}
    @enums = []
  end

  # Read all proto file in schema dir
  def read_proto_files
    # Dir.chdir("schema")
    Dir.glob("#{RELPATH}/proto/*.proto").each do |filename|
	  puts "process #{filename}..."
      @current_package_name    = nil
      @package_name            = nil

      File.open filename, "r" do |file|
	    lineno = 0
        while line = file.gets
		  lineno += 1
		  
		  begin
			  read_package_name line unless @package_name
			  handle_current_message if read_message_name line
			  read_instance_variables line
			  read_nested_message line
			  read_enums line
			  next if read_target_from_comment line
		  rescue StandardError=>e
			puts "Error in line #{lineno}"
			raise e
		  end
        end
      end
    end
  end

  def read_enums line
     line = line.force_encoding "UTF-8"
     if line =~ /^\s+enum\s+(\w+).*$/
       @current_enum = OpenStruct.new
       @current_enum.name = $1
       @current_enum.message_name = @current_message_name
     end

     if @current_enum && line =~ /\s+\}.*/
       @enums << @current_enum
       @current_enum = nil
     end

     if @current_enum && line.include?("=")
       @current_enum.fields ||= []
       @current_enum.fields  << line.split("=")[0]
     end
  end

  def read_nested_message line
    line = line.force_encoding "UTF-8"
    if line =~  /^\s+message\s+(\w+)/
      @nested_messages[@current_message_name] ||= []
      @nested_messages[@current_message_name] << $1
      @current_nested_message = $1
    end

    if line =~ /^\s+\}\s+$/
      @current_nested_message = nil
    end
  end

  def inferIvType(package, instance_variable_type)
    includedInPackage = @packages.find{|p| p[:package_name] == package}[:messages].collect{|a| a.fetch(:message_name)}.include? instance_variable_type

    if ["int", "long", "string", "bool"].include?(instance_variable_type)
      instance_variable_type
    elsif instance_variable_type.include?(".")
      instance_variable_type
    elsif includedInPackage
      package + "." + instance_variable_type
    elsif instance_variable_type == "Position"
      instance_variable_type
    else
      instance_variable_type
    end
  end

  def read_instance_variables line
    line = line.force_encoding "UTF-8"
    return unless line =~ /^\s+(required|optional|repeated)/
      package_class_name = "#{@package_name}_#{@current_message_name}"

    iv = OpenStruct.new
    iv.name = line.scan(/\S+/)[2]
    iv.instance_type = case line.scan(/\S+/)[1]
                       when "int32" then "int"
                       when "int64" then "long"
                       when "string" then "string"
                       when "bool" then "bool"
                       else line.scan(/\S+/)[1]
                       end
    iv.is_array = line =~ /repeated/ ? true : false

    if @current_nested_message
      @nested_messages_ivs[@current_nested_message] ||= []
      @nested_messages_ivs[@current_nested_message] << iv
    else
      @instance_variables[package_class_name] ||= []
      @instance_variables[package_class_name] << iv
    end
  end


  def read_package_name(line)
    line = line.force_encoding "UTF-8"
    return false unless line =~ /package\s+(\w+).*?@(.*)/
      @package_name = $1
    params = eval $2
    @package_id = params[:id]
    raise "There are two protobuf files: #{@package_name} and #{@packages[@package_id][:package_name]} share the same id, please verify!! #{@package_id}" if @packages[@package_id]
    @packages[@package_id] = {
      :package_name       => @package_name,
      :messages           => []
    }
    @package_target = params[:target]
    true
  end

  def read_target_from_comment(line)
    line = line.force_encoding "UTF-8"
    return false unless line =~ /@\s*(\{.*\})/
      params = eval $1
    @message_target = params[:target]
    true
  end

  def read_message_name(line)
    line = line.force_encoding "UTF-8"
    return false unless line =~  /^message\s*(\w+)/
      @current_message_name = $1
    true
  end

  def handle_current_message
    @packages[@package_id][:messages] << {
      :message_name => @current_message_name,
      :target       => @message_target || @package_target
    }
    @message_target = nil
    true
  end

  def generate_protobuf_mapping_file
    #skate = JSON::State.new({
    #  :indent         => '  ',
    #  :space          => ' ',
    #  :object_nl      => "\n",
    #  :array_nl       => "\n"
    #})
    #File.open "./projects/hero_resource/resource/protobuf.coffee", "w+" do |file|
    #  file << "MessageList = "
    #  file << state.generate(@packages)
    #  file << "\nmodule.exports = MessageList"
    #end
    File.open BUILDPATH + "/protobuf.coffee", "w+" do |file|
      file << "module.exports = [\n"
      @packages.each do |package|
        file << "  {\n"
        file << "    \"package_name\": \"#{package[:package_name]}\",\n"
        file << "    \"messages\": [\n"
        package[:messages].each do |message|
          file << "      {\n"
          message.each_pair do |key, value|
            file << "        \"#{key}\": \"#{value}\",\n"
          end
          file << "      },\n"
        end
        file << "    ]\n"
        file << "  },\n"
      end
      file << "]"
    end
  end

  def verify
    return if @packages.compact.size == @packages.size
    empty_index = []
    @packages.each_with_index { |e, index| empty_index << index unless e } 
    raise "Can't find index: '#{empty_index.join(', ')}'"
  end

  def csharp
    File.open BUILDPATH + "/netmsg_id.cs", "w+" do |file|
	  file << "using System.Collections.Generic;\n"
      file << "namespace MobaHero {\n"

      file << "\tclass Packages {\n"
      @packages.each_with_index do |package, i|
        file << "\t\tpublic const int #{package[:package_name].capitalize} = #{i};\n"
      end
      file << "\t}\n\n"

      file << "\tclass Messages {\n"
      @packages.each do |package|
        package[:messages].each_with_index do |message, i|
          file << "\t\tpublic const int #{package[:package_name].capitalize}#{message[:message_name]} = #{i};\n"
        end
        file << "\n"
      end
      file << "\t}\n"

      file << "}"
    end
  end
  
  def csharp_msgstr
    File.open BUILDPATH + "/netmsg_str.cs", "w+" do |file|
	  file << "using System.Collections.Generic;\n"
      file << "namespace MobaHero {\n"

      file << "\tclass MsgString {\n"
	  file << "\t\tDictionary<int, string> dict = new Dictionary<int, string>();\n"
	  file << "\t\tDictionary<int, int> req2respMap = new Dictionary<int, int>();\n"
	  file << "\t\tstatic MsgString inst = new MsgString(); \n"
	  file << "\t\tpublic static MsgString Instance { get { return inst; } }\n"
	  file << "\t\tpublic string Get(int id) { string res = null; dict.TryGetValue(id, out res); return res; }\n"
	  file << "\t\tpublic int GetRespID(int reqId) { int res = -1; req2respMap.TryGetValue(reqId, out res); return res; }\n"
	  file << "\t\tprivate MsgString() {\n"
      @packages.each_with_index do |package, i|
		  package[:messages].each_with_index do |message, j|
			  msgid = (i << 16) | j
			  file << "\t\t\tdict[#{msgid}] = \"#{package[:package_name]}.#{message[:message_name]}\";\n"
		  end
      end
      
      file << "\n"
      @packages.each_with_index do |package, i|
		  package[:messages].each_with_index do |message, j|
			  msg_name = message[:message_name]
			  if not msg_name.end_with? 'Response'
				msgid = (i << 16) | j
				idx = package[:messages].find_index { |m| m[:message_name] == msg_name + 'Response' }
				if idx
					respid = (i << 16) | idx				
					file << "\t\t\treq2respMap[#{msgid}] = #{respid}; // #{package[:package_name]}.#{message[:message_name]} => #{package[:package_name]}.#{package[:messages][idx][:message_name]}\n"
				end
			  end
		  end
      end
      
	  file << "\t\t}\n"
      file << "\t}\n"
      
      file << "}"
    end
  end
  
  def csharp_vars
    template = ERB.new <<-TEMPLATE
    #pragma warning disable 0649
    using UnityEngine;
    using System;
    using System.Collections.Generic;

    namespace Hero.Json
    {
      <% @packages.each do |package| %>
      namespace <%= package[:package_name] %>{
        <% package[:messages].each do |message| %>
        public class <%= message[:message_name] %>
          {

        <% (@enums.select{|o| o.message_name == message[:message_name]}).each do |e| %>
              public enum <%= e.name %>{
                <% e.fields.each do |f| %>
                  <%= f %>,
                <% end %>
              };
        <% end %>
            <% (@nested_messages[message[:message_name]] || []).each do |nm| %>
              public class <%= nm %>
              {
              <% @nested_messages_ivs[nm].each do |nmi| %>
                public <%= nmi.instance_type %><%= "[]" if nmi.is_array %> <%= nmi.name %>;
              <% end %>
              }
            <% end %>
            <% (@instance_variables[package[:package_name] + "_" + message[:message_name]] || []).each do |iv| %>
              public <%= inferIvType(package[:package_name], iv.instance_type) %><%= "[]" if iv.is_array %> <%= iv.name %>;

            <% end %>
          }
        <% end %>
        }
      <% end %>
    }
    TEMPLATE

    File.open BUILDPATH + "/netmsg_json.cs", "w+" do |file|
      template.result(binding).each_line do |l|
        next if l =~ /^\s+$/
          file << l
      end
    end

  end

  def gen_phps
    message_template = ERB.new <<-TEMPLATE
         <?php
      return array(
        <% @packages.each_with_index do |package, pIndex| %>
        <% package[:messages].each_with_index do |message, index| %>
        "<%= package[:package_name].capitalize + '_' + message[:message_name] %>" => <%= ((pIndex << 16) | index)  %>,
          <% end %>
        <% end %>
       );

    TEMPLATE

    File.open BUILDPATH + "/protocols.ini.php", "w+" do |file|
      message_template.result(binding).each_line do |l|
        next if l =~ /^\s+$/
          file << l
      end
    end
  end
end

# git@git.gree-dev.net:chen-liu/hero_robot.git
def java
  File.open BUILDPATH + "/Generated.java", "w+" do |file|
    file << "package network.codec.mapping;\n"
    file << "import java.util.HashMap;\n"
    file << "import java.util.Map;\n"
    file << "import network.codec.mapping.Message;\n"
    file << "import network.codec.mapping.Package;\n"
    file << "\n"
    file << "public class Generated {\n"
    file << "\tpublic static Map<String, Package> s2Package = new HashMap<String, Package>();\n"
    file << "\tpublic static Map<Integer, Package> i2Package = new HashMap<Integer, Package>();\n"
    file << "\n"
    file << "\tpublic static void init()\n"
    file << "\t{\n"
    length = @packages.length - 1
    for i in 0..length do
      package_name = @packages[i][:package_name].capitalize
      file << "\t\t{\n"
      file << "\t\t\tPackage pkg = new Package();\n"
      file << "\t\t\tpkg.i2Messages = new HashMap<Integer, Message>();\n"
      file << "\t\t\tpkg.s2Messages = new HashMap<String, Message>();\n";
      file << "\t\t\tpkg.packageId = #{i};\n"
      file << "\t\t\tpkg.packageName = \"#{package_name}\";\n"

      messages = @packages[i][:messages]
      mLength = messages.length - 1
      for i in 0..mLength do
        message_name = "#{messages[i][:message_name]}"
        file << "\t\t\t{\n"
        file << "\t\t\t\tMessage msg = new Message();\n"
        file << "\t\t\t\tmsg.messageId = #{i};\n"
        file << "\t\t\t\tmsg.messageName = \"#{message_name}\";\n"
        file << "\t\t\t\tmsg.messageLite = Mapping.newMessageLite(pkg.packageName, msg.messageName);\n"
        file << "\t\t\t\tmsg.handler = Mapping.newHandler(pkg.packageName, msg.messageName);\n"
        file << "\t\t\t\tpkg.i2Messages.put(msg.messageId, msg);\n"
        file << "\t\t\t\tpkg.s2Messages.put(msg.messageName, msg);\n"
        file << "\t\t\t}\n"
      end
      file << "\t\ti2Package.put(pkg.packageId, pkg);\n"
      file << "\t\ts2Package.put(pkg.packageName, pkg);\n"
      file << "\t\t}\n"
    end
    file << "\t}\n"
    file << "}"
  end
end

pm = ProtobufMagician.new
pm.read_proto_files
pm.verify
pm.generate_protobuf_mapping_file
pm.csharp
pm.csharp_msgstr
pm.csharp_vars
pm.gen_phps

